前一天我們已經開始接觸 Android 的 Intent,知道如何從一個 Activity 跳到另外一個 Activity。
但是實際開發 App 時,通常不只是「跳過去」這麼簡單,而是:
MainActivity → 第二個 Activity → 使用者操作 → 把資料帶回 MainActivity
今天就利用課堂上的幾個實作,把 Intent、putExtra()、setResult()、ActivityResultLauncher、SharedPreferences 串在一起,最後完成一個簡易的 會員註冊+登入+飲料甜點點餐系統。
本日使用的程式包含 DrinkActivity、飲料選擇版 MainActivity、RegisterActivity、OrderActivity,以及另一個「城市 → 美食」Activity 回傳練習。
今天的重點可以濃縮成一張流程圖:
┌──────────────────────┐
│ MainActivity │
│ │
│ 選擇 / 輸入資料 │
└──────────┬───────────┘
│
│ Intent
│ putExtra()
▼
┌──────────────────────┐
│ Second Activity │
│ │
│ 使用者選擇 / 輸入資料 │
└──────────┬───────────┘
│
│ setResult()
│ Intent
▼
┌──────────────────────┐
│ MainActivity │
│ │
│ 接收結果並更新畫面 │
└──────────────────────┘
也就是:
Activity A 不只可以把資料送給 Activity B,Activity B 做完事情之後,也可以把結果再傳回 Activity A。
這正是今天最重要的觀念。


第一個範例的流程非常適合理解 Activity Result:
MainActivity
│
│ 點擊「選擇飲料」
▼
DrinkActivity
│
├── 輸入飲料
├── 選擇甜度
└── 選擇冰塊
│
│ setResult()
▼
MainActivity
│
├── 飲料名稱
├── 甜度
└── 冰塊
在 MainActivity 中,程式使用新版的 ActivityResultLauncher<Intent>:
private lateinit var getResult: ActivityResultLauncher<Intent>
接著透過:
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
)
建立結果接收器。
MainActivity 的核心程式如下:
getResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
Log.d("main", "code = ${result.resultCode}")
if (result.resultCode == drinkCode) {
val intent = result.data
if (intent != null) {
val drink = intent.getStringExtra("drink")
val sugar = intent.getStringExtra("sugar")
val ice = intent.getStringExtra("ice")
tv_drink.text = getString(
R.string.drink_label,
drink
)
tv_sugar.text = getString(
R.string.sugar_label,
sugar
)
tv_ice.text = getString(
R.string.ice_label,
ice
)
}
}
}
這裡的觀念很重要:
result.resultCode
↓
判斷是哪一種結果
result.data
↓
取得回傳 Intent
getStringExtra()
↓
把資料取出
也就是第二個 Activity 回來時,我們不只是知道「它關閉了」,還可以知道它帶了什麼資料回來。
以前可能會寫:
startActivity(intent)
但今天因為我們需要等待結果回來,所以改成:
btn_select.setOnClickListener {
val intent = Intent(
this,
DrinkActivity::class.java
)
getResult.launch(intent)
}
這裡:
getResult.launch(intent)
就代表:
開啟 DrinkActivity,而且我還在等它把結果送回來。
進入 DrinkActivity 後,程式取得:
val etDrink = findViewById<EditText>(R.id.et_drink)
val rgSugar = findViewById<RadioGroup>(R.id.rg_sugar)
val rgIce = findViewById<RadioGroup>(R.id.rg_ice)
val btnFinish = findViewById<Button>(R.id.btn_finish)
使用者可以輸入飲料名稱,再透過 RadioGroup 選擇甜度與冰塊。
按下完成之後:
btnFinish.setOnClickListener {
val drink = etDrink.text.toString()
val sugar =
findViewById<RadioButton>(
rgSugar.checkedRadioButtonId
).text.toString()
val ice =
findViewById<RadioButton>(
rgIce.checkedRadioButtonId
).text.toString()
val intent = Intent()
intent.putExtra("drink", drink)
intent.putExtra("sugar", sugar)
intent.putExtra("ice", ice)
setResult(drinkCode, intent)
finish()
}
這裡可以看到今天另外兩個主角:
setResult()
finish()
假設使用者選擇:
飲料:紅茶
甜度:半糖
冰塊:少冰
程式就會:
intent.putExtra("drink", drink)
intent.putExtra("sugar", sugar)
intent.putExtra("ice", ice)
可以把 Intent 想像成一個包裹:
Intent
┌───────────────────────────┐
│ "drink" → "紅茶" │
│ "sugar" → "半糖" │
│ "ice" → "少冰" │
└───────────────────────────┘
然後:
setResult(drinkCode, intent)
把這個包裹交還給上一個 Activity。
最後:
finish()
結束目前的 DrinkActivity。
接下來的範例就更接近真正的 App。
整個程式有三個主要 Activity:
┌─────────────────┐
│ MainActivity │
│ │
│ Name │
│ Email │
│ Password │
└───────┬─────────┘
│
┌─────────┴─────────┐
│ │
Register Login OK
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ RegisterActivity │ │ OrderActivity │
│ │ │ │
│ Name │ │ 飲料 │
│ Email │ │ 甜點 │
│ Password │ │ 數量 │
└─────────┬──────────┘ └─────────┬──────────┘
│ │
│ setResult(200) │ setResult(100)
│ │
└──────────┬─────────────┘
▼
┌──────────────┐
│ MainActivity │
│ 顯示結果 │
└──────────────┘
這次同一個 MainActivity 必須處理兩種結果:
private val registerCode = 200
private val orderCode = 100
所以我們可以利用不同的 resultCode 判斷到底是哪一個 Activity 回來的。
RegisterActivity 要求輸入:
Name
Email
Password
按下 OK 時,先判斷三個欄位是不是都有資料:
if (
editTextName.text.isNotEmpty() &&
editTextEmail.text.isNotEmpty() &&
editTextPassword.text.isNotEmpty()
) {
val name = editTextName.text.toString()
val email = editTextEmail.text.toString()
val password = editTextPassword.text.toString()
val resultIntent = Intent()
resultIntent.putExtra("name", name)
resultIntent.putExtra("email", email)
resultIntent.putExtra("password", password)
setResult(registerCode, resultIntent)
finish()
} else {
Toast.makeText(
this,
"Please fill all fields",
Toast.LENGTH_SHORT
).show()
}
這裡再次使用相同模式:
取得資料
↓
Intent()
↓
putExtra()
↓
setResult()
↓
finish()
當 RegisterActivity 回來後:
if (result.resultCode == registerCode) {
result.data?.run {
val name = getStringExtra("name")
val email = getStringExtra("email")
val password = getStringExtra("password")
tv_result.text = "Registered successfully!"
txt_name.setText(name)
txt_email.setText(email)
txt_password.setText(password)
}
}
這樣註冊頁面輸入的資料,就重新回到了 MainActivity。
今天這份範例又多了一個很實用的東西:
SharedPreferences
取得 SharedPreferences:
val sp = getSharedPreferences(
"userdata",
MODE_PRIVATE
)
寫入資料:
sp.edit().apply {
putString("name", name)
putString("email", email)
putString("password", password)
apply()
}
所以會員註冊完成之後,資料除了顯示在畫面,也會存進:
userdata
├── name
├── email
└── password
接下來就可以拿來做簡易登入驗證。
當使用者按下 Order:
val inputName = txt_name.text.toString()
val inputPassword = txt_password.text.toString()
先取得使用者目前輸入的帳號密碼。
再讀取 SharedPreferences:
val sp = getSharedPreferences(
"userdata",
MODE_PRIVATE
)
val spName = sp.getString(
"name",
"none"
)
val spPassword = sp.getString(
"password",
"none"
)
最後比較:
if (
inputName == spName &&
inputPassword == spPassword
) {
val orderIntent = Intent(
this@MainActivity,
OrderActivity::class.java
)
getResult.launch(orderIntent)
} else {
Toast.makeText(
this@MainActivity,
"The name & password are wrong",
Toast.LENGTH_SHORT
).show()
}
登入成功才可以進入點餐畫面。
今天最好玩的地方就是這個點餐系統。
程式把飲料分成兩組 Menu。



第一組:
🥤 Cola $50
🍵 Tea $60
☕ Coffee $100
程式:
rbCola.text = "Cola , $50"
ivDrink1.setImageResource(R.drawable.cola_1)
rbTea.text = "Tea , $60"
ivDrink2.setImageResource(R.drawable.blacktea)
rbCoffee.text = "Coffee , $100"
ivDrink3.setImageResource(R.drawable.latte)

![]()
![]()
按下第二組選單後變成:
🍨 Ice cream $150
🧋 Milk tea $100
🥛 Milk $80
程式:
rbCola.text = "Ice cream , $150"
ivDrink1.setImageResource(R.drawable.icecream)
rbTea.text = "Milk tea , $100"
ivDrink2.setImageResource(R.drawable.milk_tea_icon)
rbCoffee.text = "Milk , $80"
ivDrink3.setImageResource(R.drawable.milk_icon)
因此我們不用另外建立第二個 Activity,只要動態修改:
RadioButton 文字
+
ImageView 圖片
+
價格計算方式
就能讓同一個畫面切換不同 Menu。
這裡我覺得是一個很值得記下來的寫法:
private fun updateMenu(menuNum: Int) {
rgDrinks.clearCheck()
drinkNum = menuNum
if (menuNum == 1) {
rbCola.text = "Cola , $50"
ivDrink1.setImageResource(R.drawable.cola_1)
rbTea.text = "Tea , $60"
ivDrink2.setImageResource(R.drawable.blacktea)
rbCoffee.text = "Coffee , $100"
ivDrink3.setImageResource(R.drawable.latte)
} else {
rbCola.text = "Ice cream , $150"
ivDrink1.setImageResource(R.drawable.icecream)
rbTea.text = "Milk tea , $100"
ivDrink2.setImageResource(R.drawable.milk_tea_icon)
rbCoffee.text = "Milk , $80"
ivDrink3.setImageResource(R.drawable.milk_icon)
}
}
按鈕就只需要:
btnDrink1.setOnClickListener {
updateMenu(1)
}
btnDrink2.setOnClickListener {
updateMenu(2)
}
比起把整段程式複製兩次,這種方式更容易維護。
這個點餐畫面也剛好把 Android 常見的兩種選擇元件放在一起。
飲料使用:
○ Cola
○ Tea
○ Coffee
搭配:
RadioGroup
代表:
三種飲料中只能選一種。
甜點則使用:
☑ Waffle
☑ Pancake
☑ Muffin
代表:
可以一次選很多種。
所以簡單記:
RadioButton → 單選
CheckBox → 複選
這也是 Android 表單介面非常常見的設計。
這次還加入了一個我覺得很方便的小功能。
例如使用者直接在 Waffle 數量輸入:
2
程式就自動:
☑ Waffle
不用再自己點 CheckBox。
這裡使用的是:
TextWatcher
程式:
val dessertQtyWatcher = {
editText: EditText,
checkBox: CheckBox ->
object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
override fun afterTextChanged(
s: Editable?
) {
val qty =
s.toString().toIntOrNull() ?: 0
checkBox.isChecked = qty >= 1
}
}
}
接著:
etWaffleQty.addTextChangedListener(
dessertQtyWatcher(
etWaffleQty,
cbWaffle
)
)
etPancakeQty.addTextChangedListener(
dessertQtyWatcher(
etPancakeQty,
cbPancake
)
)
etMuffinQty.addTextChangedListener(
dessertQtyWatcher(
etMuffinQty,
cbMuffin
)
)
因此:
輸入 0
↓
☐ 不勾選
輸入 1
↓
☑ 自動勾選
輸入 2
↓
☑ 自動勾選
假設選:
Cola × 2
程式:
val unitPrice =
if (drinkNum == 1) 50
else 150
val name =
if (drinkNum == 1) "Cola"
else "Ice cream"
val price = unitPrice * drinkQty
total += price
如果:
Cola = $50
數量 = 2
就是:
50 × 2 = 100
再把結果加入 StringBuilder:
summary.append(
"Drink : $name -> $$unitPrice x $drinkQty = $price\n"
)
例如 Waffle:
if (cbWaffle.isChecked) {
val qty =
etWaffleQty.text
.toString()
.toIntOrNull() ?: 0
val price = 100 * qty
total += price
summary.append(
"Waffle -> $100 x $qty = $price\n"
)
}
Pancake:
if (cbPancake.isChecked) {
val qty =
etPancakeQty.text
.toString()
.toIntOrNull() ?: 0
val price = 120 * qty
total += price
summary.append(
"Pancake -> 120 x $qty = $price\n"
)
}
Muffin:
if (cbMuffin.isChecked) {
val qty =
etMuffinQty.text
.toString()
.toIntOrNull() ?: 0
val price = 80 * qty
total += price
summary.append(
"Muffin -> 80 x $qty = $price\n"
)
}
最後:
summary.append(
"\nThe total fee is $$total"
)
就完成一個簡單的購物車計價功能。
最重要的是,OrderActivity 並不是算完就結束。
它還會:
val data = tvResult.text.toString()
val intent = Intent()
intent.putExtra(
"order",
data
)
setResult(
orderCode,
intent
)
finish()
也就是:
OrderActivity
│
│ 訂單內容
│
│ putExtra("order", data)
▼
Intent
│
│ setResult(100)
▼
MainActivity
MainActivity 再判斷:
else if (
result.resultCode == orderCode
) {
val data =
result.data
?.getStringExtra("order")
tv_result.text = data
}
於是整份訂單又回到了首頁。
今天的程式資料裡還有另一個很適合練習 Activity 傳值的範例。
MainActivity 先讓使用者選:
○ 台北
○ 新北
○ 桃園
取得選取的 RadioButton:
val selectedId =
rgCity.checkedRadioButtonId
if (selectedId != -1) {
val radioButton =
findViewById<RadioButton>(
selectedId
)
val cityName =
radioButton.text.toString()
val intent =
Intent(
this,
CityActivity::class.java
)
intent.putExtra(
"CITY_NAME",
cityName
)
startActivityForResult(
intent,
citycode
)
}
CityActivity 收到:
val cityName =
intent.getStringExtra(
"CITY_NAME"
) ?: ""
接著利用:
when (cityName)
判斷城市。
例如台北:
"台北" -> {
cbFood1.text = "牛肉麵"
cbFood2.text = "滷肉飯"
cbFood3.text = "小籠包"
cbFood4.text = "芒果冰"
cbFood1.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.taipei_food1,
0
)
cbFood2.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.taipei_food2,
0
)
cbFood3.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.taipei_food3,
0
)
cbFood4.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.taipei_food4,
0
)
}
同一個 Activity 就可以依據不同城市,動態更換文字與圖片。
使用者勾選想吃的東西:
val selectedFoods =
mutableListOf<String>()
if (cbFood1.isChecked)
selectedFoods.add(
cbFood1.text.toString()
)
if (cbFood2.isChecked)
selectedFoods.add(
cbFood2.text.toString()
)
if (cbFood3.isChecked)
selectedFoods.add(
cbFood3.text.toString()
)
if (cbFood4.isChecked)
selectedFoods.add(
cbFood4.text.toString()
)
然後:
val resultIntent = Intent()
resultIntent.putExtra(
"CITY_NAME",
cityName
)
resultIntent.putExtra(
"SELECTED_FOODS",
selectedFoods.joinToString(", ")
)
setResult(
citycode,
resultIntent
)
finish()
最後 MainActivity 顯示:
tvResult.text =
"城市名稱:$cityName\n" +
"美食建議:$selectedFoods"
這就是完整的雙向 Activity 資料傳遞。
今天的範例剛好同時出現兩種寫法。
startActivityForResult(
intent,
citycode
)
接:
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(
requestCode,
resultCode,
data
)
}
城市美食範例使用的就是這個方式。
private lateinit var getResult:
ActivityResultLauncher<Intent>
搭配:
getResult =
registerForActivityResult(
ActivityResultContracts
.StartActivityForResult()
) { result ->
// 處理回傳結果
}
啟動:
getResult.launch(intent)
飲料以及會員點餐範例採用這個方式。
所以今天最大的收穫之一,就是實際看到了:
舊:
startActivityForResult()
+
onActivityResult()
↓
新:
registerForActivityResult()
+
ActivityResultLauncher
把今天全部整理成一張圖:
Android App
│
▼
┌──────────────┐
│ MainActivity │
└──────┬───────┘
│
┌─────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│RegisterActivity │ │ OrderActivity │
└────────┬────────┘ └────────┬────────┘
│ │
│ name │ drink
│ email │ dessert
│ password │ quantity
│ │ total
▼ ▼
Intent Intent
│ │
│ setResult(200) │ setResult(100)
└─────────────┬──────────────┘
▼
┌──────────────┐
│ MainActivity │
└──────┬───────┘
│
SharedPreferences
│
┌─────────┴─────────┐
│ │
name password
這時候已經不再只是單純的「Activity 跳頁」,而是開始具備一個 App 真正需要的頁面流程、狀態與資料傳遞。
今天一次練到了不少 Android 開發的重要觀念:
Intent
│
├── Activity 跳轉
│
├── putExtra()
│ └── 傳送資料
│
├── getStringExtra()
│ └── 取得資料
│
├── setResult()
│ └── 回傳資料
│
└── finish()
└── 結束 Activity
Activity Result
│
├── startActivityForResult()
│
└── ActivityResultLauncher
UI
│
├── RadioGroup
├── RadioButton
├── CheckBox
├── EditText
├── ImageView
├── TextView
└── Toast
資料處理
│
├── StringBuilder
├── MutableList
├── TextWatcher
└── SharedPreferences
從飲料選擇、城市美食,到最後的會員註冊與點餐系統,其實都是同一個核心概念:
不同 Activity 各自負責不同工作,再透過 Intent 交換資料。
Day 20 已經開始從「Android 元件怎麼用」,慢慢進入「一個 App 的功能怎麼串起來」。
以前看到:
Intent(this, SecondActivity::class.java)
可能只覺得 Intent 就是拿來換頁。
但今天實作之後,可以看到 Intent 真正有趣的地方是:
Activity A
↓
傳資料
↓
Activity B
↓
處理資料
↓
回傳結果
↓
Activity A
再搭配 SharedPreferences,就可以開始保存一些簡單的使用者資料。
今天完成的會員+點餐範例,已經有:
註冊 → 儲存資料 → 登入驗證 → 選擇商品 → 輸入數量 → 自動計價 → 回傳訂單
從最基本的 Activity 切換,一步一步組合之後,就已經開始有真正 App 的味道了!